/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package pgentity.services;

import config.CFCote;
import config.CFTemp;
import config.CFUser;
import config.PGConfig;
import db.DBContext;
import db.PGKeys;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import libCore.BackgroundServices;
import libCore.SNServices;
import libCore.config.Config;
import logging.report.PGLogCategory;
import logging.Logging;
import logging.report.ActionRecord;
import logging.report.DataRecord;
import logging.report.UserAction;
import pgentity.BoxEgg;
import pgentity.CoinCard;
import pgentity.Cote;
import pgentity.CoteList;
import pgentity.EggStore;
import pgentity.EntityContext;
import pgentity.FindEggs;
import pgentity.FriendList;
import pgentity.Inventory;
import pgentity.Mailbox3;
import pgentity.Minigame;
import pgentity.NPCList;
import pgentity.Penguindex;
import pgentity.UIData;
import pgentity.UrgentQuest;
import pgentity.UrgentQuestContext;
import pgentity.UsedGiftCode;
import pgentity.User;
import pgentity.UserDailyData;
import pgentity.UserGifts;
import pgentity.UserList;
import pgentity.UserSettings;
import pgentity.UserTempData;
import pgentity.events.release_event.ReleaseEventServices;
import pgentity.prize.PGPrize;
import pgentity.prize.PrizeFactory;
import pgentity.quest.ExpenseGoldRecord;
import pgentity.quest.LoginDayRecord;
import pgentity.quest.QuestLogger;
import pgentity.quest.UserFishChanged;
import pgentity.quest.UserGoldChanged;
import share.*;
import zme.api.exception.ZingMeApiException;

/**
 *
 * @author KieuAnh
 */
public class UserServices
{
    private UserServices()
    {
        super();
    }
    
    private static final UserServices inst = new UserServices();
    
    public static UserServices inst()
    {
        return inst;
    }
    
    public User createNewUser(String uid, String signedReq, final long now)
            throws PGException, IOException, ZingMeApiException
    {
        try
        {
            if (DBContext.Redis().sadd(PGKeys.ALL_USERS, uid) > 0L)
            {
                SNServices sns = new SNServices(signedReq);
                PGException.Assert(sns.validUser(uid), PGError.INVALID_SIGNED_REQUEST,
                        "Signed request are invalid");
                SNServices.ZMUserInfo userInfo = sns.getUserInfo();

                String name    =   userInfo.getDisplayName();
                String avatar   =   userInfo.getAvatar();

                @SuppressWarnings("deprecated")
                final User user = User.newUser(uid, name, avatar, now);      
                UserTempData uTempData = UserTempData.getTempData(uid);
                FriendList friendList = FriendList.getFriendList(uid);
                QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
                FriendServices.inst().reloadFriendList(friendList, sns, uLogger,
                        uTempData, now);

                Penguindex penguindex = Penguindex.getPenguindex(uid);
                List<String> coteConfs = PGConfig.inst()
                        .getUser().getDefaultUser().getCotes();
                CoteList coteList = CoteList.getCotes(uid);
                this.initCote(coteList, coteConfs, penguindex, now);

                this.initQuest(user, now);
                initNPCs(user, now);

                user.saveToDB();

                Logging.log(DataRecord.make(PGLogCategory.DANG_KY_MOI, user, now));

                return user;
            }
        }
        catch (Exception ex)
        {
            DBContext.Redis().srem(PGKeys.ALL_USERS, uid);
        }
        
        return null;
    }
    
    public User createTestUser(String uid, String name, String ava, final long now)
            throws PGException, IOException, ZingMeApiException
    {
        if (DBContext.Redis().sadd(PGKeys.ALL_USERS, uid) > 0L)
        {
            @SuppressWarnings("deprecated")
            final User user = User.newUser(uid, name, ava, now);
            
            Penguindex penguindex = Penguindex.getPenguindex(uid);
            List<String> coteConfs = PGConfig.inst()
                    .getUser().getDefaultUser().getCotes();
            CoteList coteList = CoteList.getCotes(uid);
            this.initCote(coteList, coteConfs, penguindex, now);
            
            this.initQuest(user, now);
            initNPCs(user, now);
            
            user.saveToDB();
            
            return user;
        }
        
        return null;
    }
    
    private void initCote(CoteList coteList,
            List<String> coteTokens,
            Penguindex penguindex, long createdTime) throws PGException
    {
        String[] coteIDs = new String[coteTokens.size()];
        for (int i = 0; i < coteTokens.size(); ++i) {
            CFCote.Templates.Template coteConf = PGConfig.inst().getCote()
                    .templs().get(coteTokens.get(i));
            
            final String coteID = PGKeys.randomKey();
            coteIDs[i] = coteID;
            
            CoteServices.inst().createCote(coteList.getUid(),
                    coteID, coteConf, penguindex, createdTime);
        }
        
        for (String coteID : coteIDs) {
            coteList.append(coteID);
        }
    }
    
    public Cote addNewCote(String uid, String coteToken, long now)
    {
        String coteID = PGKeys.randomKey();
        Penguindex pdx = Penguindex.getPenguindex(uid);
        CFCote.Templates.Template coteConf = PGConfig.inst().getCote()
                .templs().get(coteToken);
        
        Cote cote = CoteServices.inst().createCote(uid, coteID, coteConf, pdx, now);
        CoteList.getCotes(uid).append(coteID);
        
        return cote;
    }
    
    private void initQuest(User user, long createdTime)
    {
        Iterable<String> qLines = PGConfig.inst().getMainQuest().keySet();
        for (String qLine : qLines) {
            QuestServices.inst().makeQuestLine(user, qLine);
        }
        
        QuestServices.inst().newDailyQuest(user, createdTime);
    }
    
    public void initNPCs(User user, final long now)
    {
        final CFUser.NPCs conf = PGConfig.inst().getUser().npc();
        String[] npcIDs = new String[conf.size()];
        for (Integer npcIdx : conf.keySet()) {
            UserList npcList = UserList.getList(UserList.ListType.NPC, npcIdx);
            int nNPC = npcList.size();
            if (nNPC <= 0) return;
            
            npcIDs[npcIdx] = npcList.randElem();
        }
        
        FriendList friendList = FriendList.getFriendList(user.getUid());
        friendList.add(npcIDs);
        NPCList.getNPCList(user.getUid()).append(npcIDs);
    }
    
    public synchronized static void checkMakeNPC(final long now)
    {
        System.out.println("Check make npc at thread: " + Thread.currentThread());
        UserList npcList = UserList.getList(UserList.ListType.NPC, 0);
        int currentNPC = npcList.size();
        int nNeedNPC = PGConfig.inst().temp().NPCPoolSize - currentNPC;
        if (nNeedNPC > 0)
        {
            UserServices.inst().makeNPCs(nNeedNPC, now);
        }
    }
    
    public void makeNPCs(int nNPC, final long now)
    {
        System.out.println("Make npc thread: " + Thread.currentThread());
        final CFUser.NPCs conf = PGConfig.inst().getUser().npc();
        
        for (final Integer npcIdx : conf.keySet()) {
            final String[] npcIDs = new String[nNPC];
            
            for (int i = 0; i < nNPC; i++) {
                final String npcID = PGKeys.randomKey();
                String name = conf.get(npcIdx).getName();
                String avatar = conf.get(npcIdx).getAvatar();

                User npc = User.newUser(npcID, name, avatar, now);
                Penguindex npcPenguindex = Penguindex.getPenguindex(npcID);

                CoteList coteList = CoteList.getCotes(npcID);
                initCote(coteList, conf.get(npcIdx).cotes(), npcPenguindex, now);
                initQuest(npc, now);

                FriendServices.inst().setNPCData(npc, npcIdx, now);

                npc.saveToDB();

                npcIDs[i] = npcID;
            }
            
            UserList.getList(UserList.ListType.ALL_USER).add(npcIDs);
            UserList.getList(UserList.ListType.NPC, npcIdx).add(npcIDs);
            System.out.println("Made " + nNPC + " npc " + npcIdx + " at thread: " + Thread.currentThread());
        }
    }
    
    public Collection<String> getAllUsers()
    {
        return DBContext.Redis().smembers(PGKeys.ALL_USERS);
    }
    
    public void increaseUserExp(EntityContext context, int incExp, long now, UserAction act)
    {
        int oldUserLevel = context.getUser().getLevel();
        context.getUser().increaseExp(incExp);
        this.updateLevel(context.getUser());
        
        for (int level = oldUserLevel + 1; level <= context.getUser().getLevel();
                ++level)
        {
            // prize on level up
            PGPrize prize = PrizeFactory.getPrize(PGConfig.inst()
                    .getUser().get(level).getLevelUpPrize(), UserAction.LEN_CAP);

            prize.award(context, now);
            
            // log
            Logging.log(new ActionRecord(context.getUser(), now,
                    UserAction.LEN_CAP, level));
        }
        
        if (oldUserLevel < context.getUser().getLevel())
        {
            // check main quest
            QuestServices.inst().autoAcceptMainQuest(context.getUser());
        }
        
        Logging.log(DataRecord.make(PGLogCategory.THU_EXP, context.getUser(), now, incExp, act));
    }
    
    private void updateLevel(User user)
    {
        int newLevel = PGConfig.inst().getUser().levelByExp(user.getExp());
        
        if (newLevel != user.getLevel())
        {
            user.setLevel(newLevel);
        }
    }
    
    @SuppressWarnings( "deprecation" )
    public void increaseGold(User user, QuestLogger uLogger, int incGold, long now, UserAction source)
    {
        PGException.Assert(incGold >= 0, PGError.INCREASE_NEGATIVE_GOLD,
                "Increment gold must be positive {" + incGold + "}");
        
        user.increaseGold(incGold);
        
        uLogger.log(new UserGoldChanged(user.getGold()));
        Logging.log(DataRecord.make(PGLogCategory.THU_VANG,
                user, now, incGold, source));
    }
    
    public void decreaseGold(User user, QuestLogger userQLogger, int decGold,
            long now, UserAction source)
    {
        PGException.Assert(decGold >= 0, PGError.DECREASE_NEGATIVE_GOLD,
                "Decrement gold must be positive {" + decGold + "}");
        
        if (decGold > 0)
        {
            user.decreaseGold(decGold);
            userQLogger.log(new ExpenseGoldRecord(decGold));
            userQLogger.log(new UserGoldChanged(user.getGold()));
            Logging.log(DataRecord.make(PGLogCategory.CHI_VANG,
                    user, now, decGold, source));
        }
    }
    
    public void changeFish(User user, QuestLogger uLogger, int chFish, long now)
    {
        if (chFish != 0)
        {
            user.setFish(user.getFish() + chFish);
            uLogger.log(new UserFishChanged(user.getFish()));
        }
    }
    
    public void paymentIncreaseCoin(String uid, int incCoin, long now)
            throws PGException
    {
        User user = User.getUser(uid);
        increaseCoin(user, incCoin, now, UserAction.NAP_XU);
        user.saveToDB();
    }
    
    public void increaseCoin(User user, int incCoin, long now, UserAction source)
            throws PGException
    {
        PGException.Assert(incCoin >= 0, PGError.INCREASE_NEGATIVE_COIN,
                "Increasement coin must be positive {" + incCoin + "}");
        
        if (incCoin > 0)
        {
            user.changeCoin(incCoin);
            Logging.log(DataRecord.make(PGLogCategory.THU_XU,
                    user, now, incCoin, source));
        }
    }
    
    public void decreaseCoin(User user, int decCoin, long now, UserAction source)
    {
        PGException.Assert(decCoin >= 0, PGError.INCREASE_NEGATIVE_COIN,
                "Increasement coin must be positive {" + decCoin + "}");
        
        if (decCoin > 0)
        {
            user.changeCoin(-decCoin);
            Logging.log(DataRecord.make(PGLogCategory.CHI_XU,
                    user, now, decCoin, source));
        }
    }
    
    @SuppressWarnings( "deprecation" )
    private void setGold(User user, int gold)
    {
        user.increaseGold(user.getGold() - gold);
    }
    
    public void activeCoinCard(String uid,int time_day, long now)
    {
        CoinCard cc = CoinCard.getCC(uid);
        PGException.Assert(!cc.inCC(now),
                PGError.ALREADY_IN_CC, "Already in coin card");
        
        long start = TimeUtil.getMidnight(now).getTime();
        //int durInDay = PGConfig.inst().temp().CoinCardDuration();
        long duration = TimeUnit.MILLISECONDS.convert(time_day, TimeUnit.DAYS);
        long end = start + duration;
        
        cc.setDueTime(start, end);
        cc.saveToDB();
        
        User user = User.getUser(uid);
        Logging.log(new ActionRecord(user, now, UserAction.MUA_THE_THANG,
                PGConfig.inst().temp().CoinCardPrice,
                TimeUtil.viDateFormat(start), TimeUtil.viDateFormat(end)));
    }
    
    public boolean inCoinCard(String uid, long now)
    {
        CoinCard cc = CoinCard.getCC(uid);
        return cc.inCC(now);
    }
    
    public void registerLogin(QuestLogger qLogger, String uid,
            String signedReq, long now)
    {
        qLogger.log(new LoginDayRecord(now));
        
        UserTempData uTempData = UserTempData.getTempData(uid);
        long lastLogin = PGHelper.toLong(
                uTempData.getData(PGMacro.LAST_LOGIN));
        uTempData.setData(PGMacro.LAST_LOGIN, now);
        
        int nDays = TimeUtil.diffDays(lastLogin, now);
        
        if (nDays >= 1) // pass new day
        {
            Logging.log(DataRecord.make(PGLogCategory.A1LOGIN, User.getUser(uid), now));
            
            loginPrizingUpdate(nDays, uTempData, now);
            newDayPrizing(nDays, uTempData, now);
            
            a1Backup(uid, now);
            tryRemoveNPCs(uid);
            
            SNServices sns = new SNServices(signedReq);
            if (sns.validUser(uid))
            {
                QuestLogger uLogger = QuestServices.inst().getQuestLogger(uid, now);
                syncFriendUpdate(uTempData, sns, uLogger, now);
                syncInfoUpdate(uid, sns);
            }
            
            safelyFix(uid);
        }
    }
    
    private void loginPrizingUpdate(int nDays, UserTempData uTempData, long now)
    {
        // update repeated login
        int repLoginDay = PGHelper.toInteger(
                uTempData.getData(PGMacro.REPEATED_LOGIN_DAY));

        if (nDays == 1)
        {
            ++repLoginDay;
        }

        if (nDays > 1 || repLoginDay > 7)
        {
            repLoginDay = 1;
        }

        uTempData.setData(PGMacro.REPEATED_LOGIN_DAY, repLoginDay);

        UserDailyData uDailyData = UserDailyData.getData(uTempData.getUid(), now);
        uDailyData.setData(PGMacro.RECEIVED_LOGIN_PRIZE, false);
    }
    
    private void newDayPrizing(int nDays, UserTempData uTempData, long now)
    {
        if (nDays < 1)
        {
            return;
        }
        
        CFTemp conf = PGConfig.inst().temp();
        
        uTempData.incData(PGMacro.RAND_PRIZE_TURN2, 1);
        
        int mngMaxStock = PGConfig.inst().miniGame().getMaxStock();
        Minigame minigame = Minigame.getMinigame(uTempData.getUid());
        int currentTurn = minigame.getTurn();
        int inc = Math.min(mngMaxStock - currentTurn, conf.MinigameTurnPerDay);
        if (inc > 0)
        {
            minigame.incTurn(inc);
        }
    }
    
    private void syncFriendUpdate(UserTempData uTempData, SNServices sns,
            QuestLogger uLogger, long now)
    {
        final String uid = uTempData.getUid();
        long lastSyncFriend = PGHelper.toLong(
                uTempData.getData(PGMacro.LAST_TIME_SYNC_FRIEND_LIST));
        
        int nDays = TimeUtil.diffDays(lastSyncFriend, now);
        if (nDays >= PGConfig.inst().temp().AutoSyncFriends_Day)
        {
            try
            {
                FriendList friendList = FriendList.getFriendList(uid);
                FriendServices.inst().reloadFriendList(friendList, sns, uLogger, uTempData, now);
            } catch (Exception ex) {
                PGLog.error("syncFriendUpdate(%s): %s", uid, ex);
            }
        }
    }
    
    private void syncInfoUpdate(String uid, SNServices sns)
    {
        try {
            User user = User.getUser(uid);
            user.setName(sns.getUserInfo().getDisplayName());
            user.setAvatar(sns.getUserInfo().getAvatar());
            user.saveToDB();
        } catch (Exception ex) {
            PGLog.error("syncInfoUpdate(%s): %s", uid, ex);
        }
    }
    
    private void safelyFix(String uid)
    {
        try
        {
            EggStore.fix(Inventory.redisKey(uid).getChild(PGKeys.FD_EGGS));
            List<String> coteIDs = CoteList.getCotes(uid).getAll();
            for (String coteID : coteIDs) {
                EggStore.fix(Cote.redisKey(uid, coteID).getChild(PGKeys.FD_EGGS));
                EggStore.fix(BoxEgg.redisKey(uid, coteID).getChild(PGKeys.FD_EGGS));
            }
        } catch (Exception e)
        {
            PGLog.error("safelyFix(%s): %s", uid, e.getMessage());
        }
    }
    
    public void a1Backup(final String uid, final long now)
    {
        Object userData = dumpUser(uid);
        final String userJSONData = PGHelper.obj2PrettyJSON(userData);
        BackgroundServices.inst().runBackground(new Runnable()
        {
            @Override
            public void run() {
                BufferedWriter out;
                try {
                    String dayToken = TimeUtil.dayToken(now);
                    String a1BackupDir = Config.getParam("a1backup", "directory");
                    File dayDir = new File(a1BackupDir + dayToken);
                    if (!dayDir.exists())
                    {
                        dayDir.mkdir();
                    }

                    String fileName = dayDir.getAbsolutePath() + '/'
                            + uid + ".bak";
                    File file = new File(fileName);
                    
                    int duplicated = 1;
                    while (file.exists())
                    {
                        fileName = dayDir.getAbsolutePath() + '/'
                            + uid + "_" + (++duplicated) + ".bak";
                        file = new File(fileName);
                    }
                    
                    file.createNewFile();
                    out = new BufferedWriter(new FileWriter(file, false));

                    out.write(userJSONData);

                    out.close();
                } catch (IOException ex) {
                    Logger.getLogger(UserServices.class.getName())
                            .log(Level.SEVERE, null, ex);
                }
            }
        });
    }
    
    private Object dumpUser(String uid)
    {
        Map<String, Object> dumpData = new HashMap();
        
        dumpData.put(PGMacro.RELEASE_EVENT, ReleaseEventServices.SERVICES.dump(uid));
        dumpData.put(PGMacro.USER, User.getUser(uid).dump());
        dumpData.put(PGMacro.NPC_LIST, dumpNPCs(uid));
        dumpData.put(PGMacro.FRIEND_LIST, FriendServices.inst().dumpFriendList(uid));
        dumpData.put(PGMacro.MAIN_QUEST, QuestServices.inst().dumpMainQuest(uid));
        dumpData.put(PGMacro.ACHIEVEMENTS, QuestServices.inst().dumpAchievements(uid));
        dumpData.put(PGMacro.USER_TEMP_DATA, UserTempData.getTempData(uid).dump());
        dumpData.put(PGMacro.SETTINGS, UserSettings.getEntity(uid).dump());
        dumpData.put(PGMacro.PENGUINDEX, Penguindex.getPenguindex(uid).dump());
        dumpData.put(PGMacro.UIDATA, UIData.getEntity(uid).dump());
        dumpData.put(PGMacro.INVENTORY, Inventory.getInventory(uid).dump());
        dumpData.put(PGMacro.GIFTS, UserGifts.getGift(uid).dump());
        dumpData.put(PGMacro.USED_GIFTCODE, UsedGiftCode.getUsedGiftCode(uid).dump());
        dumpData.put(PGMacro.COIN_CARD, CoinCard.getCC(uid).dump());
        dumpData.put(PGMacro.MINIGAME, Minigame.getMinigame(uid).dump());
        dumpData.put(PGMacro.NPC_FIND_EGGS, FindEggs.getFindEggs(uid).dump());
        dumpData.put(PGMacro.URGENT_QUEST, UrgentQuest.getUQ(uid).dump());
        
        CoteList coteList = CoteList.getCotes(uid);
        List<String> coteIDs = coteList.getAll();
        List<Object> cotes = new ArrayList(coteList.length());
        for (String coteID : coteIDs) {
            cotes.add(CoteServices.inst().dumpCote(uid, coteID));
        }
        dumpData.put(PGMacro.COTE, cotes);
        
        int dbVersion = User.getUser(uid).getDbVer();
        return AMFBuilder.make(PGMacro.DB_VERSION, dbVersion, "data", dumpData);
    }
    
    private Object dumpNPCs(String uid)
    {
        return NPCList.getNPCList(uid).length();
    }
    
    private void tryRemoveNPCs(String uid)
    {
        NPCList npcs = NPCList.getNPCList(uid);
        if (npcs.length() <= 0)
        {
            return;
        }
        
        User user = User.getUser(uid);
        FriendList friendList = FriendList.getFriendList(uid);
        if (user.getLevel() >= PGConfig.inst().temp().AutoRemoveNPC_Level ||
            friendList.size() >= PGConfig.inst().temp().AutoRemoveNPC_Friends)
        {
            destroyNPCs(npcs);
        }
    }
    
    public void destroyNPCs(NPCList npcs)
    {
        String uid = npcs.getUid();
                
        List<String> allNPCs = npcs.getAll();
        String[] arrNPCs = allNPCs.toArray(new String[allNPCs.size()]);

        FriendList friends = FriendList.getFriendList(uid);
        friends.remove(arrNPCs);

        NPCList.destroyNPCList(uid);
    }
    
    public void destroyUser(String uid)
    {
        ReleaseEventServices.SERVICES.destroy(uid);
        FriendList.destroy(uid);
        QuestServices.inst().destroyMainQuest(uid);
        QuestServices.inst().destroyAchievements(uid);
        QuestServices.inst().destroyUrgentQuest(uid);
        destroyNPCs(NPCList.getNPCList(uid));
        Mailbox3.destroy(uid);
        UserTempData.destroy(uid);
        UserSettings.destroy(uid);
        Penguindex.destroy(uid);
        UIData.destroy(uid);
        Inventory.destroy(uid);
        GiftServices.inst().destroyGifts(uid);
        UsedGiftCode.destroy(uid);
        CoinCard.destroy(uid);
        Minigame.destroy(uid);
        FindEggs.destroy(uid);
        
        CoteList coteList = CoteList.getCotes(uid);
        List<String> coteIDs = coteList.getAll();
        for (String coteID : coteIDs) {
            CoteServices.inst().destroyCote(uid, coteID);
        }
        CoteList.destroy(uid);
        
        User.destroy(uid);
        UserList.getList(UserList.ListType.ALL_USER).remove(uid);
    }
}